CloudFrontの署名付きURL(signed URL)で、「名前をつけて保存」のファイル名を指定する
CloudFrontでは署名付きURL(signed URL)が利用できます。
このとき、「名前をつけて保存」のファイル名を指定する方法を試してみました。
おすすめの方
- CloudFrontをCloudFormationで作成したい方
- CloudFrontの署名付きURLを利用したい方
- CloudFrontの署名付きURLをboto3で発行したい方
- CloudFrontの署名付きURLで「名前をつけて保存」のファイル名を指定したい方
ライブラリをインストールする
pip install cryptography
pip install boto3
署名の準備をする
公開鍵と秘密鍵を作成し、公開鍵をCloudFrontに登録する
openssl genrsa -out private_key.pem 2048
openssl rsa -pubout -in private_key.pem -out public_key.pem
cat public_key.pem | pbcopy
CloudFrontに登録します。
キーIDが必要になるので、メモしておきます。
パラメータストアにキーIDと秘密鍵を登録する
キーIDをスクリプトに書いたり、秘密鍵をローカルで利用しても良いのですが、せっかくなのでパラメータストアに登録します。 (Lambdaで実行する場合の想定です。)
aws ssm put-parameter \
--name "/CloudFront/TestKeyId" \
--type "String" \
--value "K3HET2C9J3NLWC" \
--overwrite
aws ssm put-parameter \
--name "/CloudFront/TestPrivateKey" \
--type "SecureString" \
--value file://private_key.pem \
--overwrite
CloudFrontを作成する
テンプレートファイル
バケットポリシーは、Getのみを指定します。また、CachePolicyでwhitelistに「response-content-disposition」を設定しています。
AWSTemplateFormatVersion: "2010-09-09"
Description: CloudFront Stack
Parameters:
TestKeyId:
Type: AWS::SSM::Parameter::Value<String>
Default: /CloudFront/TestKeyId
Resources:
TestBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub cloudfront-s3-test-${AWS::AccountId}-${AWS::Region}
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
TestBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref TestBucket
PolicyDocument:
Id: TestBucket-BucketPolicy
Statement:
- Effect: Allow
Action:
- s3:GetObject
Resource:
- !Sub arn:aws:s3:::${TestBucket}/*
Principal:
Service: cloudfront.amazonaws.com
Condition:
StringEquals:
AWS:SourceArn:
!Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${TestDistribution}
TestOriginAccessControl:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Name: TestOriginAccessControl
OriginAccessControlOriginType: s3
SigningBehavior: always
SigningProtocol: sigv4
TestKeyGroup:
Type: AWS::CloudFront::KeyGroup
Properties:
KeyGroupConfig:
Name: test-key-group
Items:
- !Ref TestKeyId
TestDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
- Id: !Sub S3-${TestBucket}
DomainName: !GetAtt TestBucket.RegionalDomainName
OriginAccessControlId: !GetAtt TestOriginAccessControl.Id
S3OriginConfig: {}
Enabled: true
DefaultRootObject: index.html
DefaultCacheBehavior:
TargetOriginId: !Sub S3-${TestBucket}
AllowedMethods:
- HEAD
- GET
CachePolicyId: !Ref CachePolicy
ViewerProtocolPolicy: https-only
TrustedKeyGroups:
- !Ref TestKeyGroup
HttpVersion: http2
CachePolicy:
Type: AWS::CloudFront::CachePolicy
Properties:
CachePolicyConfig:
Name: test-cache-policy
DefaultTTL: 1
MinTTL: 1
MaxTTL: 1
ParametersInCacheKeyAndForwardedToOrigin:
CookiesConfig:
CookieBehavior: none
HeadersConfig:
HeaderBehavior: none
QueryStringsConfig:
QueryStringBehavior: whitelist
QueryStrings:
- response-content-disposition
EnableAcceptEncodingGzip: true
デプロイ
aws cloudformation deploy \
--template-file cloudfront.yaml \
--stack-name CloudFront-Test-Stack \
--capabilities CAPABILITY_NAMED_IAM \
--no-fail-on-empty-changeset
適当なファイルをS3バケットに置く
適当なテキストを作成して、S3バケットに格納します。
echo 'hello, world' > test.txt
aws s3 cp test.txt s3://cloudfront-s3-test-AwsAccountId-ap-northeast-1
CloudFrontの署名付きURLを利用して、データの取得を試す
署名付きURLを発行するスクリプト
指定するファイル名を「アルファベットのみ」と「日本語あり」の2種類で試します。
- hello.txt
- はろー.txt
import boto3
from datetime import datetime
from zoneinfo import ZoneInfo
from urllib.parse import quote
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from botocore.signers import CloudFrontSigner
BASE_URL = "https://xxx.cloudfront.net"
ssm = boto3.client("ssm")
def main():
filename = "hello.txt"
response_content_disposition_value1 = quote(f'attachment; filename="{filename}"')
url1 = f"{BASE_URL}/test.txt?response-content-disposition={response_content_disposition_value1}"
signed_url1 = get_signed_url(
url1,
datetime(2024, 11, 8, 21, 00, 0, tzinfo=ZoneInfo("Asia/Tokyo")),
)
# -----
filename_jp = quote("はろー.txt".encode("utf-8"))
response_content_disposition_value2 = quote(
f"attachment; filename=\"hello.txt\"; filename*=UTF-8''{filename_jp}"
)
url1 = f"{BASE_URL}/test.txt?response-content-disposition={response_content_disposition_value2}"
signed_url2 = get_signed_url(
url1,
datetime(2024, 11, 8, 21, 00, 0, tzinfo=ZoneInfo("Asia/Tokyo")),
)
# -----
with open("test.html", "w") as f:
f.write(f'<a href="{signed_url1}">Download</a>')
f.write("<br /> <br />")
f.write(f'<a href="{signed_url2}">Download</a>')
def rsa_signer(data):
# https://github.com/boto/boto3/blob/develop/boto3/examples/cloudfront.rst
res = ssm.get_parameter(Name="/CloudFront/TestPrivateKey", WithDecryption=True)
private_key = serialization.load_pem_private_key(
res["Parameter"].get("Value").encode(),
password=None,
backend=default_backend(),
)
return private_key.sign(data, padding.PKCS1v15(), hashes.SHA1())
def get_key_id():
res = ssm.get_parameter(Name="/CloudFront/TestKeyId")
return res["Parameter"].get("Value")
def get_signed_url(target_url, expire_date):
key_id = get_key_id()
cloudfront_signer = CloudFrontSigner(key_id, rsa_signer)
return cloudfront_signer.generate_presigned_url(
target_url, date_less_than=expire_date
)
if __name__ == "__main__":
main()
スクリプトを実行する
python app.py
ブラウザでHTMLファイルを開き、ダウンロードする
それぞれ、名前をつけて保存時のデフォルトのファイル名が表示されました。
さいごに
CloudFrontの署名付きURL(signed URL)で、「名前をつけて保存」のファイル名を指定する方法を試してみました。参考になれば幸いです。